IReadOnly

Rapid overview

IReadOnlyCollection & IReadOnlyList β€” Immutable Access

"Use IReadOnly* interfaces to expose collections without allowing modification."

❌ Bad example:

public class Portfolio
{
    private List<Position> _positions = new();

    public List<Position> Positions => _positions; // exposes internal list
}

// Caller can mutate internal state
var portfolio = new Portfolio();
portfolio.Positions.Add(new Position()); // breaks encapsulation
portfolio.Positions.Clear(); // disaster!

Exposing mutable collections lets callers break invariants and bypass validation.

βœ… Good example:

public class Portfolio
{
    private List<Position> _positions = new();

    public IReadOnlyCollection<Position> Positions => _positions;

    public void AddPosition(Position position)
    {
        ValidatePosition(position);
        _positions.Add(position);
    }
}

// Caller can only read
var portfolio = new Portfolio();
int count = portfolio.Positions.Count; // βœ… allowed
// portfolio.Positions.Add(...); // ❌ compiler error

πŸ‘‰ IReadOnlyCollection provides Count and iteration, but no Add/Remove.

πŸ”₯ Using IReadOnlyList for indexed access:

public class TradingDay
{
    private List<Trade> _trades = new();

    public IReadOnlyList<Trade> Trades => _trades;

    public void RecordTrade(Trade trade)
    {
        _trades.Add(trade);
    }
}

// Caller can access by index, but not modify
var day = new TradingDay();
var firstTrade = day.Trades[0]; // βœ… indexed access
int count = day.Trades.Count;   // βœ… count
// day.Trades.Add(...);          // ❌ compiler error

πŸ‘‰ IReadOnlyList adds indexed access without exposing mutability.

πŸ”₯ Avoiding defensive copies:

// ❌ Bad: creates unnecessary copy
public IEnumerable<Order> GetOrders()
{
    return _orders.ToList(); // allocates new list every call
}

// βœ… Good: exposes read-only view without copying
public IReadOnlyCollection<Order> GetOrders()
{
    return _orders; // no allocation, just interface cast
}

πŸ‘‰ List implements IReadOnlyCollection, so casting is free.

πŸ’‘ In trading systems:

  • Expose position snapshots as IReadOnlyCollection to prevent accidental modifications.
  • Return IReadOnlyList for price history where indexed access is useful.
  • Prevent invariant violations by hiding Add/Remove while keeping data accessible.

---

Questions & Answers

Q: What's the difference between IReadOnlyCollection and IReadOnlyList?

A: IReadOnlyList extends IReadOnlyCollection and adds indexed access (this[int]). Use IReadOnlyList when callers need random access without mutation.

*Q: Does IReadOnly guarantee immutability?**

A: No. It prevents modification through the interface, but underlying data can still change. If the backing List is modified, IReadOnlyCollection reflects changes. Use ReadOnlyCollection for true immutability.

Q: Can I cast List to IReadOnlyCollection?

A: Yes. List implements IReadOnlyCollection and IReadOnlyList. Casting is a zero-cost abstractionβ€”no allocation or copying.

Q: What's ReadOnlyCollection vs IReadOnlyCollection?

A: ReadOnlyCollection is a wrapper class that prevents modification entirely, even if you have the underlying list. IReadOnlyCollection is an interface; if you cast back to List, you can mutate.

Q: Should I return IReadOnlyList instead of IEnumerable?

A: If Count and indexed access are useful to callers and data is already materialized (not lazy), yes. IReadOnlyList is more informative without exposing mutability.

Q: How do I create a truly immutable collection?

A: Use ImmutableList<T> from System.Collections.Immutable. Modifications return new instances. Or wrap with new ReadOnlyCollection<T>(list).

Q: Can I expose arrays as IReadOnlyList?

A: Yes, arrays implement IReadOnlyList. But callers can cast back to T[] and mutate. Use Array.AsReadOnly(array) for true protection.

Q: What's the performance of IReadOnlyList vs List?

A: Identical. IReadOnlyList is just an interface restriction. Indexing and iteration have the same performance as the underlying collection.

Q: How do I mock IReadOnlyCollection in tests?

A: Use arrays or List directly. Most mocking frameworks can stub IReadOnlyCollection, but real collections are often simpler in tests.

Q: When should I use IReadOnlyCollection over IEnumerable?

A: When Count is useful to callers and data is already materialized. IEnumerable is better for lazy sequences or when hiding collection semantics.